Docker Build
docker-build (VMID 112, 10.0.0.12) is the CI build server. It runs the self-hosted CI runners and the private Docker registry that every other VM pulls its images from.
| Playbook | ansible/hetzner-main/setup-docker-build.yml |
| Registry stack | ansible/hetzner-main/docker-build-apps/docker-registry/ |
| Make targets | setup-docker-build, update-docker-build, setup-gitea-runner, update-gitea-runner, deploy-docker-registry, enroll-wazuh-docker-build |
| Secrets | Docker Registry Password, ABAIR Github Runner, Gitea Runner Registration Token |
CI runners
Two runners work independently on the same VM. Each has its own Linux user and SSH key pair.
| GitHub Actions runner | Gitea Actions runner | |
|---|---|---|
| Linux user | github-runner | gitea-runner |
| Registered with | the phonlab-tcd GitHub organisation | Gitea at https://gitea-direct.abair.ie |
| Version | actions-runner 2.316.1 | act_runner 0.2.11 |
| Service | GitHub's svc.sh systemd service | act_runner.service |
| Labels | GitHub defaults | self-hosted:host (jobs run directly on the VM, not in a container), name docker-build-gitea |
| Set up by | make setup-docker-build | make setup-gitea-runner (the gitea-runner tag) |
Both users are in the docker group and are logged in to the registry, so workflows can docker build and docker push without extra credentials. Node.js 20 is installed for JavaScript actions such as actions/checkout.
The Gitea runner registers with gitea-direct.abair.ie, which skips Cloudflare, rather than gitea.abair.ie. Cloudflare cuts off the runner's long-poll connections. See Secops → Gitea.
Deploy keys
Each runner's SSH public key is authorized for the deploy user on the VMs it deploys to. The other playbooks fetch the keys straight from docker-build at run time, so nothing is copied by hand:
| Target VM | GitHub runner key | Gitea runner key |
|---|---|---|
| services-user | yes | no |
| services-staff | yes | yes |
| api-gateway | yes | no |
A deploy step in CI is then a plain SSH command, for example ssh deploy@10.0.0.19 "cd /opt/apps/<app> && docker compose pull && docker compose up -d".
Registering a runner
Registration only happens on the first run. The playbook skips it once a runner is configured.
- GitHub: the token must be a short-lived runner registration token, not a personal access token. Get one from the organisation's Actions → Runners → New self-hosted runner page. The
ABAIR Github Runneritem in Vaultwarden will have expired, so pass a fresh token:make setup-docker-build TARGET_GITHUB_TOKEN=<fresh token> - Gitea: create a registration token under Site Administration → Actions → Runners, store it in Vaultwarden as
Gitea Runner Registration Token, then runmake setup-gitea-runner.
Docker registry
| Image | registry:2, container docker-registry |
| Address | 10.0.0.12:5000 inside the network, registry.abair.ie through the webserver |
| Auth | htpasswd, user admin, password Docker Registry Password |
| Storage | /opt/apps/docker-registry/data |
| Deploy | make deploy-docker-registry |
The registry serves TLS itself, using certificates in /opt/apps/docker-registry/certs/. Clients inside the network connect by IP, so every VM lists 10.0.0.12:5000 as an insecure registry in /etc/docker/daemon.json. The auth/ and certs/ directories were copied by hand from the legacy services VM, and Ansible doesn't create them.
The registry holds the only copy of the built images. On a rebuild, restore its data from the VM backup before configuring the other VMs, or their docker compose pull will fail.
Maintenance
- A root cron job runs
docker system prune -af --filter until=24hat 03:00 every day, to keep the build cache from filling the 50 GB disk. - Docker restarts on every setup or update run, which briefly restarts the registry.
- This VM has no UFW firewall configured.
- Don't resize it with
terraform apply. Use theqm setprocedure described on the Webserver page.